package com.lxm.enigma.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.lxm.enigma.algorithm.Algorithm;
import com.lxm.enigma.core.EnigmaConfig;
import com.lxm.enigma.wrappers.EnigmaRequestWrapper;
import com.lxm.enigma.wrappers.EnigmaResponseWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;

/**
 * @Author: Lys
 * @Date 2022/5/31
 * @Describe
 **/
@Slf4j
public class EnigmaChainFilter implements Filter {

    private final EnigmaConfig enigmaConfig;
    private final Algorithm algorithm;

    public EnigmaChainFilter(EnigmaConfig enigmaConfig, Algorithm algorithm) {
        this.enigmaConfig = enigmaConfig;
        this.algorithm = algorithm;
    }

    /**
     * chain是链表，filter的进入位置在servlet进入时，出口位置在servlet返回时。
     * * 从controller进入到回复，chain会执行4次. 此方法会跳转4次
     * * chai.doFilter的位置很重要
     *
     * @param servletRequest
     * @param servletResponse
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        HttpServletResponse resp = (HttpServletResponse) servletResponse;
        String uri = req.getRequestURI();
        String method = req.getMethod();
        String fullUri = method + ":" + uri;

        log.debug("enigma-filter , fullUri : {}", fullUri);

        boolean enc = false;
        boolean dec = false;

        var encStreamList = this.enigmaConfig.getEncStreamArray();
        var encDataList = this.enigmaConfig.getEncDataArray();
        var decList = this.enigmaConfig.getDecArray();

        // 没有添加任何加解密，或者调试模式，跳过
        if ((CollectionUtils.isEmpty(encStreamList) && CollectionUtils.isEmpty(encDataList) && CollectionUtils.isEmpty(decList))
                || enigmaConfig.isDebug()) {
            chain.doFilter(req, resp);
            return;
        }

        if (contains(fullUri, decList)) {
            dec = true;
        }
        if (contains(fullUri, encStreamList)) {
            enc = true;
        }
        // 如果都没有，跳出
        if (!dec && !enc) {
            chain.doFilter(req, resp);
            return;
        }

        EnigmaResponseWrapper responseWrapper = null;
        EnigmaRequestWrapper requestWrapper = null;
        // 解密先处理，加密后处理，chain是有顺序的

        // 以下判断看起来很乱。目的是在加解密的状态下，让其对应的EnigmaResponseWrapper和EnigmaRequestWrapper参与chain的后续流程
        if (dec) {
            requestWrapper = new EnigmaRequestWrapper(req);
            try {
                decrypt(requestWrapper, req);
            } catch (RuntimeException e) {
                // 如果进来这里，说明aes校验未通过，直接返回错误
                respondInvalidAesCipher(resp);
                return;
            }
        }
        if (enc) {
            responseWrapper = new EnigmaResponseWrapper(resp);
        }
        if (dec && enc) {
            chain.doFilter(requestWrapper, responseWrapper);
        } else if (enc) {
            chain.doFilter(req, responseWrapper);
        } else {
            chain.doFilter(requestWrapper, resp);
        }

        if (enc) {
            String responseData = responseWrapper.getResponseData();
            encrypt(responseData, resp);
        }
    }

    /**
     * 加密
     *
     * @param content
     * @param response
     * @throws IOException
     */
    private void encrypt(String content, ServletResponse response) throws IOException {
        log.debug("enigma-filter,encrypt ori content : {}", content);
        String cipherContent = null;
        try {
            cipherContent = algorithm.encipher(content, enigmaConfig.getKey());
        } catch (Exception e) {
            log.error("encrypt error ", e);
        }
        respond(cipherContent, response, null);
    }

    /**
     * 解密
     *
     * @param requestWrapper
     * @param req
     */
    private void decrypt(EnigmaRequestWrapper requestWrapper, HttpServletRequest req) {
        String requestData = requestWrapper.getRequestData();
        if (StringUtils.isNotBlank(requestData)) {
            log.debug("enigma-filter , decrypt ori content: {}", requestData);
        }
        try {
            // body有传值，且是post方法，解密body。在后续controller里会自动转换成object
            if (StringUtils.isNotBlank(requestData) && StringUtils.equals(req.getMethod(), RequestMethod.POST.name())) {
                String decryptRequestData = algorithm.decipher(requestData, enigmaConfig.getKey());
                log.debug("enigma-filter , decrypted content: {}", decryptRequestData);
                requestWrapper.setRequestData(decryptRequestData);
            }
            // url参数解密
            Map<String, String> paramMap = new HashMap<>();
            Enumeration<String> parameterNames = req.getParameterNames();
            while (parameterNames.hasMoreElements()) {
                String paramName = parameterNames.nextElement();
                //String prefixUri = req.getMethod().toLowerCase() + ":" + uri;
                String paramValue = formatParameter(req.getParameter(paramName));
                String decryptParamValue = algorithm.decipher(paramValue, enigmaConfig.getKey());
                paramMap.put(paramName, decryptParamValue);
            }
            requestWrapper.setParamMap(paramMap);
        } catch (Exception e) {
            log.error("请求数据解密失败", e);
            throw new RuntimeException("", e);
        }
    }

    @SafeVarargs
    private boolean contains(String uri, List<String>... pools) {
        var one = Arrays.stream(pools).filter(pool -> pool.contains(uri)).findFirst().orElse(null);
        return Objects.nonNull(one);
    }

    /**
     * 如果是url带参的请求，sha1加密过后的串里的+号会被转义，漏失为空格，此处补上缺漏的+
     *
     * @param param
     * @return
     */
    private String formatParameter(String param) {
        return StringUtils.replace(param, " ", "+");
    }

    private void respondInvalidAesCipher(ServletResponse response) throws IOException {
        var json = new HashMap<String,Object>();
        json.put("code",-8);
        json.put("message","aes串校验失败或格式、长度错误");
        json.put("data",null);
        var om = new ObjectMapper();
        String content = om.writeValueAsString(json);
        respond(content, response, "application/json");
    }

    /**
     * response.write
     * @param content
     * @param response
     * @param contentType
     * @throws IOException
     */
    private void respond(String content, ServletResponse response, String contentType) throws IOException {
        ServletOutputStream out = null;
        try {
            log.debug("response content : {}", content);
            response.setCharacterEncoding(enigmaConfig.getResponseCharset());
            // 指定类型时填写
            if (StringUtils.isNotBlank(contentType)) {
                response.setContentType(contentType);
            } else {
                // 未指定类型则时string串，给个length吧
                response.setContentLength(content.length());
            }
            out = response.getOutputStream();
            out.write(content.getBytes(enigmaConfig.getResponseCharset()));
        } catch (Exception e) {
            log.error("enigma encrypt response error", e);
            throw new RuntimeException(e);
        } finally {
            if (out != null) {
                out.flush();
                out.close();
            }
        }
    }
}
